/**
 * エラー情報
 */
export class ErrorInfo {
  /**
   * コンストラクタ（初期化）
   * @param {Array.<Object.<string>>=} messages エラーメッセージオブジェクト配列
   */
  constructor(messages = []) {
    // チェック結果（エラーメッセージ）
    //   _messages: [
    //     { name: 'age',    message: 'error message' },
    //     { name: 'gender', message: 'error message' },
    //     ...
    //   ]
    this._messages = messages;
  }

  /**
   * エラーメッセージを追加します（破壊的メソッド）
   * @param {string|Object} name 項目名（識別子）
   * @param {string} message エラーメッセージ
   * @return {this} 自オブジェクト
   */
  add(name, message) {
    name = String(name);
    this._messages.push({ name, message });
    return this;
  }

  /**
   * エラーメッセージを置換します（破壊的メソッド）
   * @param {string|Object} name 項目名（識別子）
   * @param {string} message エラーメッセージ
   * @return {this} 自オブジェクト
   */
  put(name, message) {
    name = String(name);
    const index = this._messages.findIndex(obj => obj.name === name);
    if (index >= 0) {
      this._messages[index] = { name, message };
    } else {
      this.add(name, message);
    }
    return this;
  }

  /**
   * エラーメッセージを削除します（破壊的メソッド）
   * @param {string|Object} name 項目名（識別子）
   * @return {this} 自オブジェクト
   */
  remove(name) {
    name = String(name);
    this._messages = this._messages.filter(obj => obj.name !== name);
    return this;
  }

  /**
   * エラーの有無を判定します。
   * @param {...string|...Object} names 項目名（識別子）の可変長引数
   * @return {boolean} 1つでもエラーメッセージを含むなら、true
   */
  has(...names) {
    return names.some(name => {
      name = String(name);
      return this._messages.some(obj => obj.name === name);
    });
  }

  /**
   * 個別にエラーメッセージを1つ取得します。
   * @param {...string|...Object} names 項目名（識別子）の可変長引数（省略した場合、先頭のエラーメッセージ）
   * @return {string} エラーメッセージ
   */
  get(...names) {
    // ヒットした1件目を返す
    let message = '';
    if (names.length === 0) {
      message = (this._messages[0] || {}).message || '';
    } else {
      names.some(name => {
        name = String(name);
        message = (this._messages.find(obj => obj.name === name) || {}).message || '';
        return message !== '';
      });
    }
    return message;
  }

  /**
   * すべてのエラーメッセージを取得します。
   * @return {Array.<string>} エラーメッセージ
   */
  all() {
    return this._messages.map(obj => obj.message);
  }

  /**
   * 正常判定（isValidのエイリアス）
   * @return {boolean} true:エラーなし, false:エラーあり
   */
  get ok() {
    return !this.isError;
  }

  /**
   * エラー判定（isErrorのエイリアス）
   * @return {boolean} true:エラーあり, false:エラーなし
   */
  get ng() {
    return this.isError;
  }

  /**
   * 正常判定（okのエイリアス）
   * @return {boolean} true:エラーなし, false:エラーあり
   */
  get isValid() {
    return !this.isError;
  }

  /**
   * エラー判定（ngのエイリアス）
   * @return {boolean} true:エラーあり, false:エラーなし
   */
  get isError() {
    return this._messages.length > 0;
  }

  /**
   * エラーメッセージオブジェクト配列からエラー情報オブジェクトを生成します。
   * @private
   * @param {Array.<Object.<string>>} messages エラーメッセージオブジェクト配列
   * @return {ErrorInfo} 新しいエラー情報オブジェクト
   */
  static _create(messages) {
    return new ErrorInfo(Array.from(messages));
  }

  /**
   * エラーなしエラー情報オブジェクトを返します（初期値用）。
   * @return {ErrorInfo} エラーなしエラー情報オブジェクト（エラーメッセージなし）
   */
  static noneError() {
    return new ErrorInfo();
  }

  /**
   * 現在のエラー情報オブジェクトのコピーを返します（Shallow Copy）。
   * @return {ErrorInfo} コピーされたエラー情報オブジェクト
   */
  copy() {
    return this.constructor._create(this._messages);
  }

  /**
   * エラー情報をマージした新しいエラー情報オブジェクトを返します。
   * @param {ErrorInfo} that マージ対象エラー情報オブジェクト
   * @return {ErrorInfo} マージ後の新しいエラー情報オブジェクト
   */
  merge(that) {
    return that._messages.reduce((merge, obj) => merge.put(obj.name, obj.message), this.copy());
  }

  /**
   * エラー情報に追加結合した新しいエラー情報オブジェクトを返します。
   * @param {ErrorInfo} that 追加結合対象エラー情報オブジェクト
   * @return {ErrorInfo} 追加結合後の新しいエラー情報オブジェクト
   */
  concat(that) {
    return this.constructor._create(this._messages.concat(that._messages));
  }

  /**
   * すべて、または、特定のエラーメッセージをクリアします。
   * @param {Array.<string|Object>=} names 項目名（識別子）の配列（省略した場合、すべてのエラーメッセージを削除します）
   * @return {ErrorInfo} クリア後の新しいエラー情報オブジェクト
   */
  clear(names = []) {
    return (names.length === 0)
      ? this.constructor.noneError()                                    // 全エラークリア
      : names.reduce((copy, name) => copy.remove(name), this.copy());   // 特定エラーのみクリア
  }
}

/**
 * 単項目チェックを行います。
 * @param {string|Object} name 入力項目名
 * @param {string|number} value 入力値
 * @param {Object=} singleValidation 単項目チェック定義
 * @param {boolean=} validateRequried 必須入力チェックを行う場合、true（デフォルト:false）
 * @return {ErrorInfo} チェック結果
 */
export function validateSingle(name, value, singleValidation = {}, validateRequried = false) {
  let result = ErrorInfo.noneError();

  const single = singleValidation[name];
  if (!single) {
    return result;
  }

  // 途中でエラーがあれば、そこでチェックを終了する
  single.validations.some(validation => {
    if (validation.isRequried && !validateRequried) {
      // チェック不要
      return false;
    }
    // チェック実行
    if (validation.validate(value)) {
      // エラーなし
      return false;
    } else {
      // エラーあり
      const message = validation.errorMessage(single.label, value);
      result.add(name, message);
      return true;
    }
  });

  return result;
}

/**
 * 相関チェックを行います。
 * @param {Object} inputData 入力内容
 * @param {Object=} multipleValidation 相関チェック定義
 * @param {Array.<string>=} categories 相関チェック対象種別配列（省略した場合、相関チェック定義にある全チェックが対象）
 * @return {ErrorInfo} チェック結果
 */
export function validateMultiple(inputData, multipleValidation = {}, categories = []) {
  let result = ErrorInfo.noneError();

  if (categories.length === 0) categories = Object.keys(multipleValidation);
  categories.forEach(category => {
    const multiple = multipleValidation[category];
    if (!multiple) {
      return;
    }

    // 途中でエラーがあれば、そこでチェックを終了する
    multiple.validations.some(validation => {
      // チェック実行
      if (validation.validate(inputData)) {
        // エラーなし
        return false;
      } else {
        // エラーあり
        const message = validation.errorMessage(multiple.label, inputData);
        result.add(category, message);
        return true;
      }
    });
  });

  return result;
}

/**
 * 全項目チェックを行います。
 * @param {Object} inputData 入力内容
 * @param {Object=} singleValidation 単項目チェック定義
 * @param {Object=} multipleValidation 相関チェック定義
 * @param {Array.<string>=|Array.<Object>=} names 単項目チェック対象項目配列（省略した場合、単項目チェック定義にある全チェックが対象）
 * @param {Array.<string>=} categories 相関チェック対象種別配列（省略した場合、相関チェック定義にある全チェックが対象）
 * @return {ErrorInfo} チェック結果
 */
export function validateAll(inputData, singleValidation = {}, multipleValidation = {}, names = [], categories = []) {
  let result = ErrorInfo.noneError();

  // 単項目チェック
  if (names.length === 0) names = Object.keys(singleValidation);
  names.forEach(name => {
    const value = inputData[name];
    const check = validateSingle(name, value, singleValidation, true);
    if (check.isError) {
      result = result.merge(check);
    }
  });

  // 単項目チェックでエラーの場合は、相関チェックは行わない
  if (result.isError) {
    return result;
  }

  // 相関チェック
  const check = validateMultiple(inputData, multipleValidation, categories);
  if (check.isError) {
    result = result.merge(check);
  }

  return result;
}
